satkit 0.22.0

Satellite Toolkit
Documentation
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# Orbital Mean-Element Messages\n",
    "\n",
    "Orbital Mean-Element Messages (OMMs) are a standardized data product defined by the CCSDS *Orbit Data Messages* standard ([CCSDS 502.0-B-3](../../guide/references/#ccsds502)) for exchanging satellite orbital elements in a machine-readable way. They contain the same orbital parameters as TLEs but in a more structured format, and are growing in popularity as the modern replacement for TLE distribution.\n",
    "\n",
    "OMMs are available from [CelesTrak](https://celestrak.org) and [Space-Track](https://www.space-track.org) in multiple encodings:\n",
    "\n",
    "- **JSON** — the most common and straightforward format\n",
    "- **XML** — more verbose with deeper hierarchy, but widely supported\n",
    "- **KVN** — key-value notation; imposes very little structure\n",
    "\n",
    "`satkit` supports SGP4 propagation of OMMs represented as Python dictionaries. KVN is **not** supported. The helper `sk.omm_from_url(url)` fetches an OMM endpoint, auto-detects JSON vs XML, and returns a list of dictionaries that can be passed directly to `sk.sgp4` — no external HTTP or XML libraries needed."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1",
   "metadata": {},
   "source": [
    "## Example 1: JSON Format\n",
    "\n",
    "Load a JSON OMM for the International Space Station from CelesTrak, propagate with SGP4, and convert to geodetic coordinates. The JSON format maps directly to a Python dictionary, making it simple to work with.\n",
    "\n",
    "> **Note:** The live fetch is wrapped in a `try`/`except`: CelesTrak throttles repeated identical GP queries (HTTP 503 — its usage policy asks for at most one request per object every ~2 hours), so when the fetch is unavailable the notebook falls back to a pinned element set embedded below. Cache what you download rather than re-fetching in a loop.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2",
   "metadata": {},
   "outputs": [],
   "source": [
    "import satkit as sk\n",
    "\n",
    "# Fetch the latest ephemeris for the International Space Station (ISS).\n",
    "# sk.omm_from_url auto-detects JSON vs XML response format and returns a list\n",
    "# of OMM dictionaries that can be passed directly to sk.sgp4.\n",
    "url = \"https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=json\"\n",
    "# A pinned OMM for the ISS (epoch 2026-08-29T15:50:02.012064), used when the live\n",
    "# fetch is unavailable — CelesTrak throttles repeated identical queries.\n",
    "# sk.sgp4 accepts this dictionary directly.\n",
    "ISS_OMM = {\n",
    "    \"OBJECT_NAME\": \"ISS (ZARYA)\",\n",
    "    \"OBJECT_ID\": \"1998-067A\",\n",
    "    \"EPOCH\": \"2026-08-29T15:50:02.012064\",\n",
    "    \"MEAN_MOTION\": 15.48929126,\n",
    "    \"ECCENTRICITY\": 0.0004994,\n",
    "    \"INCLINATION\": 51.6318,\n",
    "    \"RA_OF_ASC_NODE\": 296.4399,\n",
    "    \"ARG_OF_PERICENTER\": 88.0334,\n",
    "    \"MEAN_ANOMALY\": 272.1226,\n",
    "    \"EPHEMERIS_TYPE\": 0,\n",
    "    \"CLASSIFICATION_TYPE\": \"U\",\n",
    "    \"NORAD_CAT_ID\": 25544,\n",
    "    \"ELEMENT_SET_NO\": 999,\n",
    "    \"REV_AT_EPOCH\": 58314,\n",
    "    \"BSTAR\": 0.00011198,\n",
    "    \"MEAN_MOTION_DOT\": 5.709e-05,\n",
    "    \"MEAN_MOTION_DDOT\": 0\n",
    "}\n",
    "try:\n",
    "    omm = sk.omm_from_url(url)\n",
    "except RuntimeError as e:\n",
    "    print(f\"Live fetch unavailable ({e}); using the pinned OMM\")\n",
    "    omm = [ISS_OMM]\n",
    "\n",
    "# Get a representative time from the OMM epoch\n",
    "epoch = sk.time(omm[0][\"EPOCH\"])\n",
    "# Create a list of times — once every 10 minutes\n",
    "time_array = [epoch + sk.duration(minutes=i * 10) for i in range(6)]\n",
    "\n",
    "# TEME (inertial) output from SGP4\n",
    "pTEME, _vTEME = sk.sgp4(omm[0], time_array)\n",
    "\n",
    "# Rotate to Earth-fixed\n",
    "pITRF = [sk.frametransform.rotation(sk.frame.TEME, sk.frame.ITRF, t) * p for t, p in zip(time_array, pTEME)]\n",
    "\n",
    "# Geodetic coordinates of space station at given times\n",
    "coord = [sk.itrfcoord(x) for x in pITRF]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3",
   "metadata": {},
   "source": "## Example 2: XML Format\n\n`sk.omm_from_url` automatically detects the response format. Passing an XML endpoint returns the same list-of-dicts shape as the JSON version, so the downstream SGP4 call is identical — no `xmltodict` plumbing or manual tree traversal required. Here we fetch the same ISS OMM in XML, propagate it over roughly one orbit, and plot the ground track."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4",
   "metadata": {},
   "outputs": [],
   "source": "import json\nimport tempfile\nimport os\n\n# --- JSON file -----------------------------------------------------------\n# Write a small single-satellite OMM JSON file, standing in for a\n# Space-Track / CelesTrak bulk download saved to disk.\nomm_json = \"\"\"[{\n  \"OBJECT_NAME\": \"ISS (ZARYA)\", \"OBJECT_ID\": \"1998-067A\",\n  \"EPOCH\": \"2021-10-02T14:10:59.999\", \"TIME_SYSTEM\": \"UTC\",\n  \"MEAN_ELEMENT_THEORY\": \"SGP4\",\n  \"MEAN_MOTION\": \"15.48915330\", \"ECCENTRICITY\": \"0.0007417\",\n  \"INCLINATION\": \"51.6432\", \"RA_OF_ASC_NODE\": \"351.4697\",\n  \"ARG_OF_PERICENTER\": \"130.5364\", \"MEAN_ANOMALY\": \"329.6482\",\n  \"BSTAR\": \"0.0001027\", \"MEAN_MOTION_DOT\": \"0.00016717\",\n  \"MEAN_MOTION_DDOT\": \"0\"\n}]\"\"\"\nwith tempfile.NamedTemporaryFile(\"w\", suffix=\".json\", delete=False) as fh:\n    fh.write(omm_json)\n    json_path = fh.name\n\n# sk.omm_from_file detects JSON vs XML from the content and returns the\n# same list of dicts as sk.omm_from_url. (Plain json.load works too: sgp4\n# accepts the raw dict, quoted numbers and all.)\nomm_list = sk.omm_from_file(json_path)\nos.unlink(json_path)\n\nepoch = sk.time(omm_list[0][\"EPOCH\"])\np_json, v_json = sk.sgp4(omm_list[0], epoch + sk.duration(minutes=30))\nprint(f\"Position from JSON file (m): {p_json}\")\n\n# --- XML text ------------------------------------------------------------\n# The same OMM in CCSDS XML. sk.omm_from_text parses it directly; no XML\n# library or tree-walking needed.\nomm_xml = \"\"\"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ndm><omm id=\"CCSDS_OMM_VERS\" version=\"2.0\">\n  <body><segment>\n    <metadata>\n      <OBJECT_NAME>ISS (ZARYA)</OBJECT_NAME><OBJECT_ID>1998-067A</OBJECT_ID>\n      <CENTER_NAME>EARTH</CENTER_NAME><REF_FRAME>TEME</REF_FRAME>\n      <TIME_SYSTEM>UTC</TIME_SYSTEM>\n      <MEAN_ELEMENT_THEORY>SGP4</MEAN_ELEMENT_THEORY>\n    </metadata>\n    <data>\n      <meanElements>\n        <EPOCH>2021-10-02T14:10:59.999</EPOCH>\n        <MEAN_MOTION>15.48915330</MEAN_MOTION>\n        <ECCENTRICITY>0.0007417</ECCENTRICITY>\n        <INCLINATION>51.6432</INCLINATION>\n        <RA_OF_ASC_NODE>351.4697</RA_OF_ASC_NODE>\n        <ARG_OF_PERICENTER>130.5364</ARG_OF_PERICENTER>\n        <MEAN_ANOMALY>329.6482</MEAN_ANOMALY>\n      </meanElements>\n      <tleParameters>\n        <EPHEMERIS_TYPE>0</EPHEMERIS_TYPE>\n        <CLASSIFICATION_TYPE>U</CLASSIFICATION_TYPE>\n        <NORAD_CAT_ID>25544</NORAD_CAT_ID>\n        <ELEMENT_SET_NO>900</ELEMENT_SET_NO>\n        <REV_AT_EPOCH>29935</REV_AT_EPOCH>\n        <BSTAR>0.0001027</BSTAR>\n        <MEAN_MOTION_DOT>0.00016717</MEAN_MOTION_DOT>\n        <MEAN_MOTION_DDOT>0</MEAN_MOTION_DDOT>\n      </tleParameters>\n    </data>\n  </segment></body>\n</omm></ndm>\"\"\"\n\nomm_from_xml = sk.omm_from_text(omm_xml)[0]\np_xml, v_xml = sk.sgp4(omm_from_xml, epoch + sk.duration(minutes=30))\nprint(f\"Position from XML text  (m): {p_xml}\")\n\n# Same OMM, same epoch -> identical states from both formats\nassert (p_json == p_xml).all() and (v_json == v_xml).all()\nprint(\"JSON and XML routes agree.\")\n\n# --- TLE <-> OMM -----------------------------------------------------------\n# An OMM dict converts to a TLE object and back; the TLE lines are the\n# classic 69-character form of the same element set.\ntle = sk.TLE.from_omm(omm_from_xml)\nprint(\"\\n\".join(tle.to_2line()))\nprint(json.dumps(tle.to_omm(), indent=1)[:200], \"...\")\n"
  },
  {
   "cell_type": "markdown",
   "id": "8adc5639",
   "source": "## Example 3: Loading local OMM files\n\nBulk catalog downloads from Space-Track or CelesTrak usually land on disk as JSON or XML *files*. No special satkit loader is needed for these: parse them with the Python standard-library `json` module (or `xmltodict` for XML) and pass the resulting dictionary — with its standard CCSDS field names — straight to `sk.sgp4`.\n\nTwo notes on the dictionary route:\n\n- satkit consumes the SGP4-relevant fields (`EPOCH`, mean elements, `BSTAR`, mean-motion derivatives) and validates `MEAN_ELEMENT_THEORY` / `TIME_SYSTEM` if present; other CCSDS fields (mass, cross-sections, covariance, …) are simply ignored — they remain available in your dictionary.\n- For CCSDS XML, `xmltodict` produces the nested `body → segment → data` structure; satkit understands the nested `meanElements` / `tleParameters` groups directly, so you only need to navigate down to the `data` element.",
   "metadata": {}
  },
  {
   "cell_type": "code",
   "id": "7c9e6f6c",
   "source": "import json\nimport tempfile\nimport os\n\n# --- JSON file -----------------------------------------------------------\n# Write a small single-satellite OMM JSON file, standing in for a\n# Space-Track / CelesTrak bulk download saved to disk.\nomm_json = \"\"\"[{\n  \"OBJECT_NAME\": \"ISS (ZARYA)\", \"OBJECT_ID\": \"1998-067A\",\n  \"EPOCH\": \"2021-10-02T14:10:59.999\", \"TIME_SYSTEM\": \"UTC\",\n  \"MEAN_ELEMENT_THEORY\": \"SGP4\",\n  \"MEAN_MOTION\": \"15.48915330\", \"ECCENTRICITY\": \"0.0007417\",\n  \"INCLINATION\": \"51.6432\", \"RA_OF_ASC_NODE\": \"351.4697\",\n  \"ARG_OF_PERICENTER\": \"130.5364\", \"MEAN_ANOMALY\": \"329.6482\",\n  \"BSTAR\": \"0.0001027\", \"MEAN_MOTION_DOT\": \"0.00016717\",\n  \"MEAN_MOTION_DDOT\": \"0\"\n}]\"\"\"\nwith tempfile.NamedTemporaryFile(\"w\", suffix=\".json\", delete=False) as fh:\n    fh.write(omm_json)\n    json_path = fh.name\n\n# sk.omm_from_file detects JSON vs XML from the content and returns the\n# same list of dicts as sk.omm_from_url. (Plain json.load works too: sgp4\n# accepts the raw dict, quoted numbers and all.)\nomm_list = sk.omm_from_file(json_path)\nos.unlink(json_path)\n\nepoch = sk.time(omm_list[0][\"EPOCH\"])\np_json, v_json = sk.sgp4(omm_list[0], epoch + sk.duration(minutes=30))\nprint(f\"Position from JSON file (m): {p_json}\")\n\n# --- XML text ------------------------------------------------------------\n# The same OMM in CCSDS XML. sk.omm_from_text parses it directly; no XML\n# library or tree-walking needed.\nomm_xml = \"\"\"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ndm><omm id=\"CCSDS_OMM_VERS\" version=\"2.0\">\n  <body><segment>\n    <metadata>\n      <OBJECT_NAME>ISS (ZARYA)</OBJECT_NAME><OBJECT_ID>1998-067A</OBJECT_ID>\n      <CENTER_NAME>EARTH</CENTER_NAME><REF_FRAME>TEME</REF_FRAME>\n      <TIME_SYSTEM>UTC</TIME_SYSTEM>\n      <MEAN_ELEMENT_THEORY>SGP4</MEAN_ELEMENT_THEORY>\n    </metadata>\n    <data>\n      <meanElements>\n        <EPOCH>2021-10-02T14:10:59.999</EPOCH>\n        <MEAN_MOTION>15.48915330</MEAN_MOTION>\n        <ECCENTRICITY>0.0007417</ECCENTRICITY>\n        <INCLINATION>51.6432</INCLINATION>\n        <RA_OF_ASC_NODE>351.4697</RA_OF_ASC_NODE>\n        <ARG_OF_PERICENTER>130.5364</ARG_OF_PERICENTER>\n        <MEAN_ANOMALY>329.6482</MEAN_ANOMALY>\n      </meanElements>\n      <tleParameters>\n        <EPHEMERIS_TYPE>0</EPHEMERIS_TYPE>\n        <CLASSIFICATION_TYPE>U</CLASSIFICATION_TYPE>\n        <NORAD_CAT_ID>25544</NORAD_CAT_ID>\n        <ELEMENT_SET_NO>900</ELEMENT_SET_NO>\n        <REV_AT_EPOCH>29935</REV_AT_EPOCH>\n        <BSTAR>0.0001027</BSTAR>\n        <MEAN_MOTION_DOT>0.00016717</MEAN_MOTION_DOT>\n        <MEAN_MOTION_DDOT>0</MEAN_MOTION_DDOT>\n      </tleParameters>\n    </data>\n  </segment></body>\n</omm></ndm>\"\"\"\n\nomm_from_xml = sk.omm_from_text(omm_xml)[0]\np_xml, v_xml = sk.sgp4(omm_from_xml, epoch + sk.duration(minutes=30))\nprint(f\"Position from XML text  (m): {p_xml}\")\n\n# Same OMM, same epoch -> identical states from both formats\nassert (p_json == p_xml).all() and (v_json == v_xml).all()\nprint(\"JSON and XML routes agree.\")\n\n# --- TLE <-> OMM -----------------------------------------------------------\n# An OMM dict converts to a TLE object and back; the TLE lines are the\n# classic 69-character form of the same element set.\ntle = sk.TLE.from_omm(omm_from_xml)\nprint(\"\\n\".join(tle.to_2line()))\nprint(json.dumps(tle.to_omm(), indent=1)[:200], \"...\")\n",
   "metadata": {},
   "execution_count": null,
   "outputs": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.14.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}